/**
 * Copyright 2015 jianglibo@gmail.com
 *
 */
package com.m3958.logcollector.datacommons.controller;

import static org.springframework.data.rest.webmvc.ControllerUtils.EMPTY_RESOURCE_LIST;
import static org.springframework.http.HttpMethod.PUT;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.auditing.AuditableBeanWrapper;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.event.AfterCreateEvent;
import org.springframework.data.rest.core.event.AfterSaveEvent;
import org.springframework.data.rest.core.event.BeforeCreateEvent;
import org.springframework.data.rest.core.event.BeforeSaveEvent;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.SearchResourceMappings;
import org.springframework.data.rest.webmvc.ControllerUtils;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.ProfileController;
import org.springframework.data.rest.webmvc.RestMediaTypes;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
import org.springframework.data.rest.webmvc.support.ETag;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.data.web.PagedResourcesAssembler;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.core.EmbeddedWrappers;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriTemplate;

/**
 * copy from org.springframework.data.rest.webmvc.AbstractRepositoryRestController
 * 
 * @author jianglibo@gmail.com
 *         2015年12月9日
 *
 */
public class AbstractQcontroller implements ApplicationEventPublisherAware {

    static final String PROFILE_REL = "profile";
    
    protected static final String BASE_MAPPING = "/{repository}";
    protected static final List<String> ACCEPT_PATCH_HEADERS = Arrays.asList(//
            RestMediaTypes.MERGE_PATCH_JSON.toString(), //
            RestMediaTypes.JSON_PATCH_JSON.toString(), //
            MediaType.APPLICATION_JSON_VALUE);

    protected static final String ACCEPT_HEADER = "Accept";
    protected static final String LINK_HEADER = "Link";
            
    
    private static final EmbeddedWrappers WRAPPERS = new EmbeddedWrappers(false);

    private final PagedResourcesAssembler<Object> pagedResourcesAssembler;

    private final AuditableBeanWrapperFactory auditableBeanWrapperFactory;

    private RepositoryRestConfiguration config;

    private ApplicationEventPublisher publisher;

    protected final RepositoryEntityLinks entityLinks;

    /**
     * Creates a new {@link AbstractRepositoryRestController} for the given {@link PagedResourcesAssembler} and
     * {@link AuditableBeanWrapperFactory}.
     * 
     * @param pagedResourcesAssembler
     *            must not be {@literal null}.
     * @param auditableBeanWrapperFactory
     *            must not be {@literal null}.
     */
    public AbstractQcontroller(PagedResourcesAssembler<Object> pagedResourcesAssembler, AuditableBeanWrapperFactory auditableBeanWrapperFactory,
            RepositoryEntityLinks entityLinks, RepositoryRestConfiguration config) {

        Assert.notNull(pagedResourcesAssembler, "PagedResourcesAssembler must not be null!");

        this.pagedResourcesAssembler = pagedResourcesAssembler;
        this.auditableBeanWrapperFactory = auditableBeanWrapperFactory;
        this.entityLinks = entityLinks;
        this.config = config;
    }

//    protected Resources<?> entitiesToResources(Iterable<Object> entities, PersistentEntityResourceAssembler assembler, Class<?> domainType) {
//
//        if (!entities.iterator().hasNext()) {
//
//            List<Object> content = Arrays.<Object> asList(WRAPPERS.emptyCollectionOf(domainType));
//            return new Resources<Object>(content, getDefaultSelfLink());
//        }
//
//        List<Resource<Object>> resources = new ArrayList<Resource<Object>>();
//
//        for (Object obj : entities) {
//            resources.add(obj == null ? null : assembler.toResource(obj));
//        }
//
//        return new Resources<Resource<Object>>(resources, getDefaultSelfLink());
//    }

    protected Link resourceLink(RootResourceInformation resourceLink, Resource resource) {

        ResourceMetadata repoMapping = resourceLink.getResourceMetadata();

        Link selfLink = resource.getLink("self");
        String rel = repoMapping.getItemResourceRel();

        return new Link(selfLink.getHref(), rel);
    }

    /**
     * Triggers the creation of the domain object and renders it into the response if needed.
     * 
     * @param domainObject
     * @param invoker
     * @return
     */
    protected ResponseEntity<ResourceSupport> createAndReturn(Object domainObject, RepositoryInvoker invoker, PersistentEntityResourceAssembler assembler,
            boolean returnBody) {

        publisher.publishEvent(new BeforeCreateEvent(domainObject));
        Object savedObject = invoker.invokeSave(domainObject);
        publisher.publishEvent(new AfterCreateEvent(savedObject));

        PersistentEntityResource resource = returnBody ? assembler.toFullResource(savedObject) : null;

        HttpHeaders headers = prepareHeaders(resource);
        addLocationHeader(headers, assembler, savedObject);

        return ControllerUtils.toResponseEntity(HttpStatus.CREATED, headers, resource);
    }

    /**
     * Returns the default headers to be returned for the given {@link PersistentEntityResource}. Will set {@link ETag}
     * and {@code Last-Modified} headers if applicable.
     * 
     * @param resource
     *            can be {@literal null}.
     * @return
     */
    protected HttpHeaders prepareHeaders(PersistentEntityResource resource) {
        return resource == null ? new HttpHeaders() : prepareHeaders(resource.getPersistentEntity(), resource.getContent());
    }

    /**
     * Retruns the default headers to be returned for the given {@link PersistentEntity} and value. Will set {@link ETag}
     * and {@code Last-Modified} headers if applicable.
     * 
     * @param entity
     *            must not be {@literal null}.
     * @param value
     *            must not be {@literal null}.
     * @return
     */
    protected HttpHeaders prepareHeaders(PersistentEntity<?, ?> entity, Object value) {

        // Add ETag
        HttpHeaders headers = ETag.from(entity, value).addTo(new HttpHeaders());

        // Add Last-Modified
        AuditableBeanWrapper wrapper = getAuditableBeanWrapper(value);

        if (wrapper == null) {
            return headers;
        }

        Calendar lastModifiedDate = wrapper.getLastModifiedDate();

        if (lastModifiedDate != null) {
            headers.setLastModified(lastModifiedDate.getTimeInMillis());
        }

        return headers;
    }

    /**
     * Returns the {@link AuditableBeanWrapper} for the given source.
     * 
     * @param source
     *            can be {@literal null}.
     * @return
     */
    protected AuditableBeanWrapper getAuditableBeanWrapper(Object source) {
        return auditableBeanWrapperFactory.getBeanWrapperFor(source);
    }

    public ApplicationEventPublisher getPublisher() {
        return publisher;
    }

    public void setPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    /**
     * Sets the location header pointing to the resource representing the given instance. Will make sure we properly
     * expand the URI template potentially created as self link.
     * 
     * @param headers
     *            must not be {@literal null}.
     * @param assembler
     *            must not be {@literal null}.
     * @param source
     *            must not be {@literal null}.
     */
    protected void addLocationHeader(HttpHeaders headers, PersistentEntityResourceAssembler assembler, Object source) {

        String selfLink = assembler.getSelfLinkFor(source).getHref();
        headers.setLocation(new UriTemplate(selfLink).expand());
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    protected RepositoryRestConfiguration getConfig() {
        return config;
    }

    protected Resources<? extends Resource<Object>> entitiesToResources(Page<Object> page, PersistentEntityResourceAssembler assembler, Link baseLink) {
        return baseLink == null ? pagedResourcesAssembler.toResource(page, assembler) : pagedResourcesAssembler.toResource(page, assembler, baseLink);
    }

    protected Resources<Resource<Object>> entitiesToResources(Iterable<Object> entities, PersistentEntityResourceAssembler assembler) {

        List<Resource<Object>> resources = new ArrayList<Resource<Object>>();

        for (Object obj : entities) {
            resources.add(obj == null ? null : assembler.toResource(obj));
        }

        return new Resources<Resource<Object>>(resources);
    }

    protected List<Link> getCollectionResourceLinks(RootResourceInformation resourceInformation, DefaultedPageable pageable) {

        ResourceMetadata metadata = resourceInformation.getResourceMetadata();
        SearchResourceMappings searchMappings = metadata.getSearchResourceMappings();

        List<Link> links = new ArrayList<Link>();
        links.add(new Link(ProfileController.getPath(this.config, metadata), PROFILE_REL));

        if (searchMappings.isExported()) {
            links.add(entityLinks.linkFor(metadata.getDomainType()).slash(searchMappings.getPath()).withRel(searchMappings.getRel()));
        }

        return links;
    }
    
    protected Resources<?> entitiesToResources(Iterable<Object> entities, PersistentEntityResourceAssembler assembler,
            Class<?> domainType) {

        if (!entities.iterator().hasNext()) {

            List<Object> content = Arrays.<Object> asList(WRAPPERS.emptyCollectionOf(domainType));
            return new Resources<Object>(content, getDefaultSelfLink());
        }

        List<Resource<Object>> resources = new ArrayList<Resource<Object>>();

        for (Object obj : entities) {
            resources.add(obj == null ? null : assembler.toResource(obj));
        }

        return new Resources<Resource<Object>>(resources, getDefaultSelfLink());
    }
    
    protected Resources<?> entitiesToResources(Page<Object> page, PersistentEntityResourceAssembler assembler,
            Class<?> domainType, Link baseLink) {

        if (page.getContent().isEmpty()) {
            return pagedResourcesAssembler.toEmptyResource(page, domainType, baseLink);
        }

        return baseLink == null ? pagedResourcesAssembler.toResource(page, assembler) : pagedResourcesAssembler.toResource(
                page, assembler, baseLink);
    }
    
    @SuppressWarnings({ "unchecked" })
    protected Resources<?> toResources(Iterable<?> source, PersistentEntityResourceAssembler assembler,
            Class<?> domainType, Link baseLink) {

        if (source instanceof Page) {
            Page<Object> page = (Page<Object>) source;
            return entitiesToResources(page, assembler, domainType, baseLink);
        } else if (source instanceof Iterable) {
            return entitiesToResources((Iterable<Object>) source, assembler, domainType);
        } else {
            return new Resources(EMPTY_RESOURCE_LIST);
        }
    }

//    @SuppressWarnings({ "unchecked" })
//    protected Resources resultToResources(Object result, PersistentEntityResourceAssembler assembler, Link baseLink) {
//
//        if (result instanceof Page) {
//            Page<Object> page = (Page<Object>) result;
//            return entitiesToResources(page, assembler, baseLink);
//        } else if (result instanceof Iterable) {
//            return entitiesToResources((Iterable<Object>) result, assembler);
//        } else if (null == result) {
//            return new Resources(EMPTY_RESOURCE_LIST);
//        } else {
//            Resource<Object> resource = assembler.toFullResource(result);
//            return new Resources(Collections.singletonList(resource));
//        }
//    }
    
    /**
     * Merges the given incoming object into the given domain object.
     *
     * @param domainObject
     * @param invoker
     * @param httpMethod
     * @return
     */
    protected ResponseEntity<ResourceSupport> saveAndReturn(Object domainObject, RepositoryInvoker invoker,
            HttpMethod httpMethod, PersistentEntityResourceAssembler assembler, boolean returnBody) {

        publisher.publishEvent(new BeforeSaveEvent(domainObject));
        Object obj = invoker.invokeSave(domainObject);
        publisher.publishEvent(new AfterSaveEvent(domainObject));

        PersistentEntityResource resource = assembler.toFullResource(obj);
        HttpHeaders headers = prepareHeaders(resource);

        if (PUT.equals(httpMethod)) {
            addLocationHeader(headers, assembler, obj);
        }

        if (returnBody) {
            return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, resource);
        } else {
            return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT, headers);
        }
    }

    protected Link getDefaultSelfLink() {
        return new Link(ServletUriComponentsBuilder.fromCurrentRequest().build().toUriString());
    }

    public AuditableBeanWrapperFactory getAuditableBeanWrapperFactory() {
        return auditableBeanWrapperFactory;
    }

}
